// SPDX-License-Identifier: GPL-2.0
/*
 * blknor_dev.c - nor block driver code for LomboTech
 * nor block driver driver code implement
 *
 * Copyright (C) 2016-2021, LomboTech Co.Ltd.
 * Author: lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <linux/blkdev.h>
#include <mach/debug.h>
#include "norblk_dev.h"

#undef MOD_NAME
#define MOD_NAME		"NORBLK"

#ifndef assert
#define assert(x)		BUG_ON(!(x))
#endif

#define NOR_BLOCK_NAME		"norblk"

/*
 * the maximum number of partitions allowed,
 * 1 means partitions are not allowed
 */
#define NOR_BLOCK_MINORS	(32)
/* sector size: 512(1 << 9) byte */
#define MAX_REQ_SECTOR_CNT	(128)

struct norblk_device *norblk;

/* get the number of sectors in a block, sector size is 512 byte */
int get_block_sector_num(int block_size)
{
	return block_size >> SECTOR_SHIFT;
}

/* get the max number of sectors of the device */
int get_norblk_cap(struct norblk_device *dev)
{
	int num;

	num = get_block_sector_num(dev->geometry->block_size);

	return dev->geometry->block_cnt * num;
}

int norblk_read_sector(int start_sector, int sector_cnt, void *buf)
{
	int cur_sector_cnt;
	int block_sector_num = get_block_sector_num(norblk->geometry->block_size);
	int sector_offs = start_sector % block_sector_num;
	int res = 0;

	if (!buf) {
		PRT_ERR("buf is null\n");

		return res;
	}

	PRT_DBG("start sector: %d sector cnt: %d buf:0x%px\n", start_sector,
		sector_cnt, buf);

	while (sector_cnt > 0) {
		/* is not aligned */
		if (sector_offs || (sector_cnt < block_sector_num)) {
			PRT_ERR("not aligned[0x%x, 0x%x], sector_offs = 0x%x",
				start_sector, sector_cnt, sector_offs);

			return -EINVAL;
		} else {
			if (norblk->ops->read_block) {
				res = norblk->ops->read_block(
					start_sector / block_sector_num,
					sector_cnt / block_sector_num,
					buf);
				if (res) {
					PRT_ERR("nandblk read_block\n");
					PRT_ERR("[0x%x, 0x%x] fail(%i)\n",
						start_sector / block_sector_num,
						sector_cnt / block_sector_num,
						res);

					return -EIO;
				}
			} else {
				PRT_ERR("norblk:not support read func\n");

				return -EIO;
			}
		}

		cur_sector_cnt = sector_cnt / block_sector_num
				 * block_sector_num;

		buf += cur_sector_cnt << SECTOR_SHIFT;
		start_sector += cur_sector_cnt;
		sector_cnt -= cur_sector_cnt;
	}

	return res;
}

int norblk_write_sector(int start_sector, int sector_cnt, void *buf)
{
	int cur_sector_cnt;
	int block_sector_num = get_block_sector_num(norblk->geometry->block_size);
	int sector_offs = start_sector % block_sector_num;
	int res = 0;

	if (!buf) {
		PRT_ERR("buff is null\n");

		return res;
	}

	PRT_DBG("start sector: %d sector cnt: %d buf:0x%px\n", start_sector,
		sector_cnt, buf);

	while (sector_cnt > 0) {
		if (sector_offs || (sector_cnt < block_sector_num)) {
			PRT_ERR("not aligned[0x%x, 0x%x], sector_offs = 0x%x",
				start_sector, sector_cnt, sector_offs);

			return -EINVAL;
		} else {
			if (norblk->ops->write_block) {
				res = norblk->ops->write_block(
					start_sector / block_sector_num,
					sector_cnt / block_sector_num,
					buf);
				if (res) {
					PRT_ERR("nandblk write_block");
					PRT_ERR("[0x%x, 0x%x] fail(%i)",
						start_sector / block_sector_num,
						sector_cnt / block_sector_num,
						res);

					return -EIO;
				}
			} else {
				PRT_ERR("norblk:not supprot write func\n");

				return -EIO;
			}
		}

		cur_sector_cnt = sector_cnt / block_sector_num
				 * block_sector_num;

		buf += cur_sector_cnt << SECTOR_SHIFT;
		start_sector += cur_sector_cnt;
		sector_cnt -= cur_sector_cnt;
	}

	return res;
}

void norblk_write_req(struct request *req)
{
	int res;
	int start_sector;
	int sector_cnt;
	char *buf;

	PRT_DBG("[%i:%s]write[0x%lld, 0x%d]\n",
	current->pid, current->comm, blk_rq_pos(req), blk_rq_sectors(req));

	do {
		start_sector = blk_rq_pos(req);
		sector_cnt = blk_rq_cur_sectors(req);

		PRT_DBG("cur_write[0x%d, 0x%d]\n", start_sector, sector_cnt);

		if (!req->bio)
			PRT_DBG("cur req->bio is null\n");
		if (!req->bio->bi_vcnt)
			PRT_DBG("cur req->bio->bi_vcnt is null\n");
		if (!bio_data(req->bio))
			PRT_DBG("bio_data is null\n");

		buf = kmap(bio_page(req->bio)) + bio_offset(req->bio);

		res = norblk_write_sector(start_sector, sector_cnt, buf);

		kunmap(bio_page(req->bio));
		if (res) {
			PRT_ERR("write req[0x%x, 0x%x] faild(%i)\n",
				start_sector, sector_cnt, res);
			blk_end_request(req, BLK_STS_IOERR, blk_rq_bytes(req));

			return;
		}

		res = blk_end_request(req, BLK_STS_OK, blk_rq_cur_bytes(req));

	} while (res);
}

void norblk_read_req(struct request *req)
{
	int res;
	int start_sector;
	int sector_cnt;
	char *buf;

	do {
		start_sector = blk_rq_pos(req);
		sector_cnt = blk_rq_cur_sectors(req);

		buf = kmap(bio_page(req->bio)) + bio_offset(req->bio);

		res = norblk_read_sector(start_sector, sector_cnt, buf);

		kunmap(bio_page(req->bio));
		if (res) {
			PRT_ERR("no copied read req[0x%x, 0x%x] faild(%i)\n",
				start_sector, sector_cnt, res);
			blk_end_request(req, BLK_STS_IOERR, blk_rq_bytes(req));

			return;
		}

		res = blk_end_request(req, BLK_STS_OK, blk_rq_cur_bytes(req));

	} while (res);
}

void norblk_discard_req(struct request *req)
{
	blk_end_request(req, BLK_STS_OK, blk_rq_bytes(req));
}

void norblk_flush_req(struct request *req)
{
	blk_end_request(req, BLK_STS_OK, blk_rq_bytes(req));
}

void norblk_do_request(struct request *req)
{
	enum req_opf cmd = req_op(req);

	assert(req);

	switch(cmd) {
	case REQ_OP_READ:
		PRT_DBG("request operqtion read\n");
		norblk_read_req(req);
		break;
	case REQ_OP_WRITE:
		PRT_DBG("request operqtion write\n");
		norblk_write_req(req);
		break;
	case REQ_OP_DISCARD:
		PRT_DBG("request operation discard\n");
		norblk_discard_req(req);
		break;
	case REQ_OP_FLUSH:
		PRT_DBG("request operation flush\n");
		norblk_flush_req(req);
		break;
	default:
		PRT_ERR("invalid request operatino\n");
		break;
	}
}

void norblk_worker_func(struct work_struct *work)
{
	struct norblk_device *dev =
		container_of(work, struct norblk_device, worker);
	struct request_queue *rq = dev->rq;
	struct request *req = NULL;

	do {
		spin_lock_irq(rq->queue_lock);
		req = blk_fetch_request(rq);
		spin_unlock_irq(rq->queue_lock);

		if (req)
			norblk_do_request(req);
	} while (req);
}

void norblk_request_func(struct request_queue *rq)
{
	struct request *req;
	struct norblk_device *dev = rq->queuedata;

	if (!dev) {
		PRT_INFO("norblk offline\n");

		req = blk_fetch_request(rq);
		while (!req) {
			req->rq_flags |= RQF_QUIET;
			__blk_end_request_all(req, BLK_STS_IOERR);

			req = blk_fetch_request(rq);
		}

		return;
	}

	queue_work(dev->wq, &dev->worker);
}

int norblk_prep_request(struct request_queue *q, struct request *req)
{
	if (bio_has_data(req->bio)) {
		if ((blk_rq_bytes(req) <= 0) ||
		    (blk_rq_pos(req) + blk_rq_sectors(req)
		    > get_capacity(req->rq_disk))) {
			PRT_ERR("req over capacity\n");

			return BLKPREP_KILL;
		}
	}

	req->rq_flags |= RQF_DONTPREP;

	return BLKPREP_OK;
}

int norblk_open(struct block_device *dev, fmode_t mode)
{
	return 0;
}

void norblk_release(struct gendisk *gd, fmode_t mode)
{
	return;
}

int norblk_ioctl(struct block_device *dev, fmode_t mode, unsigned cmd,
		  unsigned long arg)
{
	return -ENOTTY;
}

static const struct block_device_operations nor_blkdev_ops = {
	.owner = THIS_MODULE,
	.open = norblk_open,
	.release = norblk_release,
	.ioctl = norblk_ioctl,
};

void init_norblk_dev(struct norblk_device *dev)
{
	assert(dev);

	dev->name = NOR_BLOCK_NAME;
	dev->major = NOR_BLOCK_MAJOR;
	dev->minors = NOR_BLOCK_MINORS;
	dev->ops = dev->geometry->geom_ops;
}

int setup_device(struct norblk_device *dev)
{
	assert(dev);

	spin_lock_init(&dev->lock);
	dev->rq = blk_init_queue(norblk_request_func, &dev->lock);
	if (!dev->rq) {
		PRT_ERR("init queue faild\n");
		goto out;
	}

	blk_queue_flag_set(QUEUE_FLAG_NONROT, dev->rq);
	blk_queue_flag_clear(QUEUE_FLAG_ADD_RANDOM, dev->rq);

	blk_queue_prep_rq(dev->rq, norblk_prep_request);
	blk_queue_logical_block_size(dev->rq, dev->geometry->block_size);
	blk_queue_max_hw_sectors(dev->rq, MAX_REQ_SECTOR_CNT);
	dev->rq->queuedata = dev;

	dev->gd = alloc_disk(dev->minors);
	if (!dev->gd) {
		PRT_ERR("alloc disk faild\n");
		goto out;
	}

	dev->gd->major = dev->major;
	dev->gd->first_minor = 0;
	dev->gd->fops = &nor_blkdev_ops;
	dev->gd->queue = dev->rq;
	dev->gd->private_data = dev;

	snprintf(dev->gd->disk_name, sizeof(dev->gd->disk_name), dev->name);
	set_capacity(dev->gd, get_norblk_cap(dev));
	add_disk(dev->gd);

	return 0;
out:
	return -ENOMEM;
}

void cleanup_device(struct norblk_device *dev)
{
	assert(dev);

	if (dev->gd) {
		del_gendisk(dev->gd);
		put_disk(dev->gd);
	}

	if (dev->rq)
		blk_cleanup_queue(dev->rq);
}

int norblk_register(struct norblk_geometry *geom)
{
	int res;

	res = register_blkdev(NOR_BLOCK_MAJOR, NOR_BLOCK_NAME);
	if (res) {
		PRT_ERR("nor block register faild\n");
		goto out;
	}

	norblk = kzalloc(sizeof(struct norblk_device), GFP_KERNEL);
	if (!norblk) {
		PRT_ERR("alloc norblk faild\n");
		res = -ENOMEM;
		goto unregister_blkdev;
	}

	norblk->geometry = geom;
	init_norblk_dev(norblk);

	norblk->wq = alloc_workqueue(NOR_BLOCK_NAME, 0, 0);
	if (!norblk->wq) {
		PRT_ERR("alloc workqueue faild\n");
		res = -ENOMEM;
		goto unregister_blkdev;
	}

	INIT_WORK(&norblk->worker, norblk_worker_func);

	res = setup_device(norblk);
	if (res) {
		PRT_ERR("setup norblk device faild\n");
		goto cleanup_device;
	}

	PRT_INFO("NOR block device register successd\n");

	return 0;

cleanup_device:
	cleanup_device(norblk);
unregister_blkdev:
	unregister_blkdev(NOR_BLOCK_MAJOR, NOR_BLOCK_NAME);
	kfree(norblk);
out:
	return res;
}

void norblk_unregister(struct norblk_geometry *gemo)
{
	cleanup_device(norblk);
	unregister_blkdev(NOR_BLOCK_MAJOR, NOR_BLOCK_NAME);
	kfree(norblk);
}

